5. Compilación de Resultados y Validación
5.1. Cargar las bibliotecas requeridas
El siguiente código carga las bibliotecas requeridas.
[1]:
import numpy as np # ndarrays for gridded data
import pandas as pd # DataFrames for tabular data
import geostatspy.GSLIB as GSLIB # GSLIB utilities, visualization and wrapper
import geostatspy.geostats as geostats # GSLIB methods convert to Python
import geostatspy
print('GeostatsPy version: ' + str(geostatspy.__version__))
import os # set working directory, run executables
from tqdm import tqdm # suppress the status bar
from functools import partialmethod
tqdm.__init__ = partialmethod(tqdm.__init__, disable=True)
ignore_warnings = True # ignore warnings?
import matplotlib.pyplot as plt # for plotting
import matplotlib as mpl # custom colorbar
import matplotlib.ticker as mticker # custom colorpar ticks
from matplotlib.ticker import (MultipleLocator, AutoMinorLocator) # control of axes ticks
plt.rc('axes', axisbelow=True) # plot all grids below the plot elements
if ignore_warnings == True:
import warnings
warnings.filterwarnings('ignore')
import os
import matplotlib.image as mpimg
import subprocess
import time
from matplotlib.offsetbox import AnchoredText
import matplotlib.gridspec as gridspec
import plotly.io as pio
import plotly.graph_objects as go
import plotly.express as px
import plotly.offline as py
# trimmed inverted rainbow colormap hsv removing magenta tones
cmap = mpl.colors.ListedColormap(mpl.cm.hsv(np.linspace(0.0, 0.70, 256))).reversed()
GeostatsPy version: 0.0.78
[2]:
def GSLIB2ndarray_3D(data_file, kcol,nreal, nx, ny, nz):
if nz > 1 and ny > 1:
array = np.ndarray(shape = (nreal, nz, ny, nx), dtype=object, order="F")
elif ny > 1:
array = np.ndarray(shape=(nreal, ny, nx), dtype=object, order="F")
else:
array = np.ndarray(nreal, nx, dtype=object)
with open(data_file) as f:
head = [next(f) for _ in range(2)] # read first two lines
line2 = head[1].split()
ncol = int(line2[0]) # get the number of columns
for icol in range(ncol): # read over the column names
head = next(f)
if icol == kcol:
col_name = head.split()[0]
for ineal in range(nreal):
if nz > 1 and ny > 1:
for iz in range(nz):
for iy in range(ny):
for ix in range(nx):
head = next(f)
array[ineal][nz - 1 - iz][ny - 1 - iy][ix] = head.split()[kcol]
elif ny > 1:
for iy in range(ny):
for ix in range(0, nx):
head = next(f)
array[ineal][ny - 1 - iy][ix] = head.split()[kcol]
else:
for ix in range(nx):
head = next(f)
array[ineal][ix] = head.split()[kcol]
return array, col_name
Nuestro punto de partida sera la base de datos compositada a 6 metros y simplificada pero completa, es decir, sin filtrar aquellas muestras por UG.
[3]:
## Cargar data simplificada de la sección anterior
import pandas as pd
import numpy as np
# Cargar base de datos en pandas y aproximar a dos decimales
df = pd.read_csv('data.csv', sep=',', encoding='latin1').round(2)
df['UG'] = df['UG'].astype(int) # Asegurarse de que la columna 'UG' sea de tipo entero
df # mostrar la data
[3]:
| X | Y | Z | cu_pct | UG | |
|---|---|---|---|---|---|
| 0 | 472187.34 | 6925805.69 | 4211.77 | 0.02 | 2 |
| 1 | 472187.74 | 6925806.53 | 4205.84 | 0.01 | 2 |
| 2 | 472188.09 | 6925807.38 | 4199.92 | 0.01 | 2 |
| 3 | 472188.47 | 6925808.28 | 4194.00 | 0.01 | 2 |
| 4 | 472188.88 | 6925809.22 | 4188.08 | 0.01 | 2 |
| ... | ... | ... | ... | ... | ... |
| 16877 | 471922.69 | 6925391.61 | 4022.52 | 0.09 | 4 |
| 16878 | 471924.63 | 6925394.53 | 4017.65 | 0.11 | 4 |
| 16879 | 471926.59 | 6925397.45 | 4012.78 | 0.18 | 4 |
| 16880 | 471928.55 | 6925400.36 | 4007.92 | 0.15 | 4 |
| 16881 | 471930.53 | 6925403.26 | 4003.05 | 0.21 | 4 |
16882 rows × 5 columns
[4]:
# Estadísticas por UG (Number, Max, Min, Mean, Variance)
# Asegúrese de haber ejecutado previamente la celda que carga 'df' (data.csv)
import pandas as pd
# lista de UGs a reportar (ajustar si hay más o diferentes!!!)
ug_list = [2, 3, 4]
stats = {}
for ug in ug_list:
serie = df.loc[df['UG'] == ug, 'cu_pct']
stats[f'UG {ug}'] = serie.agg(['count', 'max', 'min', 'mean', 'var'])
# estadisticas globales (todas las muestras)
global_stats = df['cu_pct'].agg(['count', 'max', 'min', 'mean', 'var'])
# montar DataFrame final con el orden solicitado - AJUSTAR SI HAY MÁS UGS!!!
df_stats = pd.DataFrame({
'UG 2': stats['UG 2'],
'UG 3': stats['UG 3'],
'UG 4': stats['UG 4'],
'Global': global_stats
})
# Renombrar índices a los labels solicitados
df_stats.index = ['Número', 'Máximo', 'Mínimo', 'Media', 'Varianza']
# Mostrar con 2 decimales (ajustar si lo desea)
from IPython.display import display
display(df_stats.round(2))
# Opcional: exportar a csv para revisión fuera del notebook y ajustar decimales
# df_stats.round(6).to_csv('./03_resultados/summary_stats_UGs.csv')
| UG 2 | UG 3 | UG 4 | Global | |
|---|---|---|---|---|
| Número | 6203.00 | 2048.00 | 8631.00 | 16882.00 |
| Máximo | 2.03 | 1.64 | 6.72 | 6.72 |
| Mínimo | 0.00 | 0.00 | 0.00 | 0.00 |
| Media | 0.06 | 0.13 | 0.25 | 0.17 |
| Varianza | 0.01 | 0.02 | 0.05 | 0.04 |
5.2. Cargar modelos de UGs y Leyes
El siguiente código carga nuestras estimaciones previas, tanto de UGs como de leyes.
[5]:
xmin = 471116.24; xmax = 473023.89 # rango de valores x
ymin = 6924721.57; ymax = 6926164.63 # rango de valores y
zmin = 2950.02; zmax = 4445.78 # rango de valores z
xsiz = 15; ysiz = 15; zsiz = 15 # tamaño de celda
nx = 128; ny = 97; nz = 100 # número de celdas
tmin = -999; tmax = 999; # límites de recorte de datos
# calculo automático del centro de las capas y cortes para plotear, no modificar
icx = int(nx/2); cx = xmin + icx*xsiz
icy = int(ny/2); cy = ymin + icy*ysiz
icz = int(nz/2); cz = zmin + icz*zsiz
[6]:
# Cargar modelos de UGs
modelo_ug = GSLIB2ndarray_3D("./03_resultados/modelo_ug.dat ", 0, 1, nx, ny, nz)[0][0].astype(int)
# Cargar modelos de leyes
estCU_UG2 = GSLIB2ndarray_3D("./03_resultados/modelo_leyes_UG2.dat ", 0, 1, nx, ny, nz)[0][0].astype(float)
categoria_UG2 = GSLIB2ndarray_3D("./03_resultados/modelo_categorias_UG2.dat ", 0, 1, nx, ny, nz)[0][0]
estCU_UG3 = GSLIB2ndarray_3D("./03_resultados/modelo_leyes_UG3.dat ", 0, 1, nx, ny, nz)[0][0].astype(float)
categoria_UG3 = GSLIB2ndarray_3D("./03_resultados/modelo_categorias_UG3.dat ", 0, 1, nx, ny, nz)[0][0]
estCU_UG4 = GSLIB2ndarray_3D("./03_resultados/modelo_leyes_UG4.dat ", 0, 1, nx, ny, nz)[0][0].astype(float)
categoria_UG4 = GSLIB2ndarray_3D("./03_resultados/modelo_categorias_UG4.dat ", 0, 1, nx, ny, nz)[0][0]
[7]:
## Combinar modelos de leyes en un solo modelo final de acuerdo a las UGs
modelo_leyes_final = np.full((nz, ny, nx), np.nan) # inicializar con NaN
for iz in range(nz):
for iy in range(ny):
for ix in range(nx):
ug_value = modelo_ug[iz, iy, ix]
if ug_value == 2:
modelo_leyes_final[iz, iy, ix] = estCU_UG2[iz, iy, ix]
elif ug_value == 3:
modelo_leyes_final[iz, iy, ix] = estCU_UG3[iz, iy, ix]
elif ug_value == 4:
modelo_leyes_final[iz, iy, ix] = estCU_UG4[iz, iy, ix]
## Combinar modelos de categorías en un solo modelo final de acuerdo a las UGs
modelo_categorias_final = np.full((nz, ny, nx), "Sin_Categoria", dtype=object) # inicializar con "Sin_Categoria"
for iz in range(nz):
for iy in range(ny):
for ix in range(nx):
ug_value = modelo_ug[iz, iy, ix]
if ug_value == 2:
modelo_categorias_final[iz, iy, ix] = categoria_UG2[iz, iy, ix]
elif ug_value == 3:
modelo_categorias_final[iz, iy, ix] = categoria_UG3[iz, iy, ix]
elif ug_value == 4:
modelo_categorias_final[iz, iy, ix] = categoria_UG4[iz, iy, ix]
[ ]:
continuous = 'cu_pct'
x_coords = np.linspace(xmin + xsiz/2, xmax - xsiz/2, nx)
y_coords = np.linspace(ymin + ysiz/2, ymax - ysiz/2, ny)
z_coords = np.linspace(zmin + zsiz/2, zmax - zsiz/2, nz)
import plotly.io as pio
# pio.renderers.default = "colab"
pio.renderers.default = "notebook" # usar esta linea en jupyter notebook o vscode
import plotly.graph_objects as go
import plotly.express as px
import plotly.offline as py
fig = go.Figure(data=[go.Scatter3d(
x=df['X'],
y=df['Y'],
z=df['Z'],
marker=dict(color=df[continuous],
colorscale=px.colors.sequential.Rainbow[1:],
cmin=0.0,
cmax=df[continuous].quantile(0.95),
size=2.0, # Esta opcion controla el tamaño de los datos
colorbar=dict(
title='Cu [%]',
thickness=20,
# Add a border to the colorbar
outlinecolor='black', # Color of the border
outlinewidth=2, # Width of the border
bordercolor='white', # Background border color (if applicable)
borderwidth=1 # Background border width
)
),
mode='markers',
opacity=1
)])
# otras opciones para controlar el color de los elementos de la figura, borrar hovermode para desactivar etiquetas al pasar el mouse
fig.update_layout( title_text='Estimación de Cu - UG 4',
scene = dict(
xaxis = dict(
title='Este',
backgroundcolor="white",
gridcolor="gray",
showbackground=True,
zerolinecolor="white",),
yaxis = dict(
title='Norte',
backgroundcolor="white",
gridcolor="gray",
showbackground=True,
zerolinecolor="white"),
zaxis = dict(
title='Elevación',
backgroundcolor="white",
gridcolor="gray",
showbackground=True,
zerolinecolor="white",),
),
font_family="Times New Roman",
font_color="black", hovermode=False
)
# ocultar la leyenda/autonombre "trace 0" junto al colorbar
fig.update_layout(showlegend=False)
# definir cortes en x, y, z
icz = 60 # índice de corte en z (puede ajustarse), va entre 0 y nz-1
# Agregar cortes del modelo de UGs con la misma escala de colores de plotly
x_slice = np.flip(np.flipud(modelo_leyes_final[:, :, icx]), axis=1)
y_slice = np.flip(modelo_leyes_final[:, icy, :], axis=0)
z_slice = np.flip(modelo_leyes_final[nz-icz, :, :], axis=0)
# X-slice (plano constante x)
YY, ZZ = np.meshgrid(y_coords, z_coords) # shapes (nz, ny)
XX = np.full_like(YY, x_coords[icx])
# Introducir valores NaN para hacer partes de la superficie transparentes
XX[x_slice.astype(float) < 0] = np.nan
surf_x = go.Surface(
x=XX, y=YY, z=ZZ,
surfacecolor=x_slice.astype(float),
colorscale=px.colors.sequential.Rainbow[1:],
cmin=0.0,
cmax=df[continuous].quantile(0.95),
showscale=False,
opacity=1,
name=f'corte X={x_coords[icx]:.1f}',
hoverinfo='skip',
hovertemplate=None
)
# Y-slice (plano constante y)
XX, ZZ = np.meshgrid(x_coords, z_coords) # shapes (nz, nx)
YY = np.full_like(XX, y_coords[icy])
## Introducir valores NaN para hacer partes de la superficie transparentes
YY[y_slice.astype(float) < 0] = np.nan
surf_y = go.Surface(
x=XX, y=YY, z=ZZ,
surfacecolor=y_slice.astype(float),
colorscale=px.colors.sequential.Rainbow[1:],
cmin=0.0,
cmax=df[continuous].quantile(0.95),
showscale=False,
opacity=1,
name=f'corte Y={y_coords[icy]:.1f}',
hoverinfo='skip',
hovertemplate=None,
)
# Z-slice (plano constante z)
XX, YY = np.meshgrid(x_coords, y_coords) # shapes (ny, nx)
ZZ = np.full_like(XX, z_coords[icz])
## Introducir valores NaN para hacer partes de la superficie transparentes
ZZ[z_slice.astype(float) < 0] = np.nan
surf_z = go.Surface(
x=XX, y=YY, z=ZZ,
surfacecolor=z_slice.astype(float),
colorscale=px.colors.sequential.Rainbow[1:],
cmin=0.0,
cmax=df[continuous].quantile(0.95),
showscale=False,
opacity=1,
name=f'corte Z={z_coords[icz]:.1f}',
hoverinfo='skip',
hovertemplate=None
)
# Añadir superficies al figure (manteniendo la leyenda y colores consistentes)
fig.add_trace(surf_x)
fig.add_trace(surf_y)
fig.add_trace(surf_z)
fig.show()
[9]:
import plotly.graph_objects as go
import plotly.express as px
from plotly.subplots import make_subplots
ROWS = 3
COLS = 2
# Crear listado de bancos
step_size = 15
max_benches = ROWS*COLS
lims = np.arange(4250,4250-step_size*max_benches,-step_size)
continuous = 'cu_pct'
# Initialize figure with subplots
# Initialize figure with subplots
fig = make_subplots(
rows=ROWS, cols=COLS, horizontal_spacing=0.2, vertical_spacing=0.1,
subplot_titles=[f"Cu [%], banco {lims[i]+step_size/2}" for i in range(len(lims))]
)
for i, lim in enumerate(lims):
data_temp = df.loc[(df['Z'] > lim) & (df['Z'] <= lim + step_size)]
# Calculate row and column indices for the subplot (1-based indexing)
row = i // COLS + 1
col = i % COLS + 1
# agregar Z-slice del modelo_leyes_final correspondiente al banco (plano constante z)
z_mid = lim + step_size/2.0
iz = int(np.abs(z_coords - z_mid).argmin()) # índice de la capa más cercana al centro del banco
# extraer y orientar la rebanada (debe quedar shape (ny, nx) con y_coords como filas)
z_slice2d = np.flip(modelo_leyes_final[nz-iz, :, :].astype(float), axis=0)
# enmascarar valores inválidos (-999) para que sean transparentes en el heatmap
z_slice2d[z_slice2d <= -998] = np.nan
heat = go.Heatmap(
x=x_coords,
y=y_coords,
z=z_slice2d,
colorscale=px.colors.sequential.Rainbow[1:],
zmin=0.0,
zmax=df[continuous].quantile(0.95),
showscale=False,
opacity=0.7,
hoverinfo='skip',
name=f'Z-slice {z_coords[iz]:.1f}'
)
# añadir primero el heatmap (fondo) y luego el scatter (muestras) conserva la visibilidad
fig.add_trace(heat, row=row, col=col)
fig.add_trace(
go.Scatter(
x=data_temp['X'],
y=data_temp['Y'],
marker=dict(
color=data_temp[continuous],
colorscale=px.colors.sequential.Rainbow[1:],
cmin=0.0,
cmax=df[continuous].quantile(0.95),
size=5.0,
line=dict(
color='black',
width=0.5
),
),
mode='markers',
opacity=1,
showlegend=False
),
row=row, col=col
)
# Update xaxis properties
fig.update_xaxes(title_text="Este", range=[df['X'].quantile(0.015), df['X'].quantile(0.97)], gridcolor='rgba(0,0,0,0.2)', row=row, col=col)
# Update yaxis properties
fig.update_yaxes(title_text="Norte", range=[df['Y'].quantile(0.03), df['Y'].quantile(0.99)], gridcolor='rgba(0,0,0,0.2)', row=row, col=col)
fig.update_annotations(font_size=12)
# otras opciones para controlar el color de los elementos de la figura
fig.update_layout(font_family="Times New Roman",font_size=8,
font_color="black", plot_bgcolor='white', autosize = False,
width = 600,
height = 900, hovermode=False)
fig.show()
[25]:
from plotly.subplots import make_subplots
## truncate Z variable in the df axis every x meters
df_swath = df.copy()
dz = 50 # truncar cada 50 metros
df_swath['Z'] = df_swath['Z'].where(df_swath['Z'] % dz == 0, other=df_swath['Z'] - df_swath['Z'] % dz)
## Plot truncated z values against filtered average cut > 0 percentage with a line plot and marginal counter histogram
import plotly.express as px
import plotly.graph_objects as go
# prepare aggregated line data
agg = df_swath[df_swath['cu_pct'] >= 0].groupby('Z').agg(
mean_cut=('cu_pct', 'mean'),
count=('cu_pct', 'count')
).reset_index()
# create subplot: top = line, bottom = marginal histogram (shared x)
# enable a secondary y-axis for the bottom subplot so we can plot the bar on the right
fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
vertical_spacing=0.05,
row_heights=[0.7, 0.3],
specs=[[{"type": "xy"}],
[{"type": "xy", "secondary_y": True}]])
# line trace (top)
fig.add_trace(
go.Scatter(
x=agg['Z'],
y=agg['mean_cut'],
mode='lines+markers',
name='Ley Promedio Cu [%] en muestras',
line=dict(color='blue'),
marker=dict(size=6, color='blue')
),
row=1, col=1
)
## add z vs filtered line
# line trace (top)
fig.add_trace(
go.Scatter(
x=z_coords,
y=[np.mean(np.mean(modelo_leyes_final[nz-iz-1, :, :][modelo_leyes_final[nz-iz-1, :, :] > 0])) for iz in range(nz)],
mode='lines',
name='Ley Promedio Cu (%) en modelo',
line=dict(color='red')
),
row=1, col=1
)
# marginal histogram (bottom)
fig.add_trace(
go.Histogram(
x=df_swath[df_swath['cu_pct'] >= 0]['Z'],
nbinsx=30,
marker=dict(color='blue'),
opacity=0.3,
name='Conteo muestras'
),
row=2, col=1
)
# secondary axis: count of valid cells (>0) per model z-layer
valid_counts = [
int(np.sum((modelo_leyes_final[nz - iz - 1, :, :] > 0) & np.isfinite(modelo_leyes_final[nz - iz - 1, :, :])))
for iz in range(nz)
]
bar = go.Bar(
x=z_coords,
y=valid_counts,
name='N° celdas válidas (modelo)',
marker=dict(color='red'),
opacity=0.3
)
## add bar y-axis on the right, row=2, col=1.
# add the bar to the bottom subplot using the secondary y-axis
fig.add_trace(bar, row=2, col=1, secondary_y=True)
# Update 2nd y-axis properties for the bar chart
fig.update_yaxes(
title_text='Celdas válidas (modelo)',
secondary_y=True,
row=2, col=1
)
# layout and axis labels
fig.update_xaxes(title_text='Elevación (m)', row=2, col=1)
fig.update_yaxes(title_text='Ley Promedio Cu (%)', row=1, col=1)
fig.update_yaxes(title_text='Conteo muestras', row=2, col=1)
fig.update_yaxes(title_text='Celdas válidas (modelo)', row=2, col=1, secondary_y=True)
fig.update_layout(
title='Ley Promedio de Cu vs Elevación (m)',
font_family="Times New Roman",
font_size=12,
font_color="black",
plot_bgcolor='white',
hovermode=False,
showlegend=True,
bargap=0.1,
height=600,
width=900
)
fig.show()
[28]:
## esta vez para UG 4
from plotly.subplots import make_subplots
## truncate Z variable in the df axis every x meters
df_swath = df[df['UG'] == 4].copy()
dz = 50 # truncar cada 50 metros
df_swath['Z'] = df_swath['Z'].where(df_swath['Z'] % dz == 0, other=df_swath['Z'] - df_swath['Z'] % dz)
## Plot truncated z values against filtered average cut > 0 percentage with a line plot and marginal counter histogram
import plotly.express as px
import plotly.graph_objects as go
# prepare aggregated line data
agg = df_swath[df_swath['cu_pct'] >= 0].groupby('Z').agg(
mean_cut=('cu_pct', 'mean'),
count=('cu_pct', 'count')
).reset_index()
# create subplot: top = line, bottom = marginal histogram (shared x)
# enable a secondary y-axis for the bottom subplot so we can plot the bar on the right
fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
vertical_spacing=0.05,
row_heights=[0.7, 0.3],
specs=[[{"type": "xy"}],
[{"type": "xy", "secondary_y": True}]])
# line trace (top)
fig.add_trace(
go.Scatter(
x=agg['Z'],
y=agg['mean_cut'],
mode='lines+markers',
name='Ley Promedio Cu [%] en muestras',
line=dict(color='blue'),
marker=dict(size=6, color='blue')
),
row=1, col=1
)
modelo_leyes_final_UG4 = np.full((nz, ny, nx), np.nan) # inicializar con NaN
for iz in range(nz):
for iy in range(ny):
for ix in range(nx):
if modelo_ug[iz, iy, ix] == 4:
modelo_leyes_final_UG4[iz, iy, ix] = estCU_UG4[iz, iy, ix]
## add z vs filtered line
# line trace (top)
fig.add_trace(
go.Scatter(
x=z_coords,
y=[np.mean(np.mean(modelo_leyes_final_UG4[nz-iz-1, :, :][modelo_leyes_final_UG4[nz-iz-1, :, :] > 0])) for iz in range(nz)],
mode='lines',
name='Ley Promedio Cu (%) en modelo',
line=dict(color='red')
),
row=1, col=1
)
# marginal histogram (bottom)
fig.add_trace(
go.Histogram(
x=df_swath[df_swath['cu_pct'] >= 0]['Z'],
nbinsx=30,
marker=dict(color='blue'),
opacity=0.3,
name='Conteo muestras'
),
row=2, col=1
)
# secondary axis: count of valid cells (>0) per model z-layer
valid_counts = [
int(np.sum((modelo_leyes_final_UG4[nz - iz - 1, :, :] > 0) & np.isfinite(modelo_leyes_final_UG4[nz - iz - 1, :, :])))
for iz in range(nz)
]
bar = go.Bar(
x=z_coords,
y=valid_counts,
name='N° celdas válidas (modelo)',
marker=dict(color='red'),
opacity=0.3
)
## add bar y-axis on the right, row=2, col=1.
# add the bar to the bottom subplot using the secondary y-axis
fig.add_trace(bar, row=2, col=1, secondary_y=True)
# Update 2nd y-axis properties for the bar chart
fig.update_yaxes(
title_text='Celdas válidas (modelo)',
secondary_y=True,
row=2, col=1
)
# layout and axis labels
fig.update_xaxes(title_text='Elevación (m)', row=2, col=1)
fig.update_yaxes(title_text='Ley Promedio Cu (%)', row=1, col=1)
fig.update_yaxes(title_text='Conteo muestras', row=2, col=1)
fig.update_yaxes(title_text='Celdas válidas (modelo)', row=2, col=1, secondary_y=True)
fig.update_layout(
title='Ley Promedio de Cu vs Elevación (m) - UG 4',
font_family="Times New Roman",
font_size=12,
font_color="black",
plot_bgcolor='white',
hovermode=False,
showlegend=True,
bargap=0.1,
height=600,
width=900
)
fig.show()
[82]:
from IPython.display import display
# entregar estadísticas de modelo_leyes_final según UG (Number, Max, Min, Mean, Variance)
ugs = [2, 3, 4]
results = {}
for ug in ugs:
mask = (modelo_ug == ug) & (modelo_leyes_final > 0)
vals = modelo_leyes_final[mask]
cnt = vals.size
if cnt > 0:
vals = vals.astype(float)
results[f'UG {ug}'] = {
'Número': int(cnt),
'Máximo': float(np.nanmax(vals)),
'Mínimo': float(np.nanmin(vals)),
'Media': float(np.nanmean(vals)),
'Varianza': float(np.nanvar(vals))
}
else:
results[f'UG {ug}'] = {'Número': 0, 'Máximo': np.nan, 'Mínimo': np.nan, 'Media': np.nan, 'Varianza': np.nan}
# Estadísticas globales (todas las celdas válidas)
mask_all = (modelo_leyes_final > 0) & np.isfinite(modelo_leyes_final)
vals_all = modelo_leyes_final[mask_all]
cnt_all = vals_all.size
if cnt_all > 0:
results['Global'] = {
'Número': int(cnt_all),
'Máximo': float(np.nanmax(vals_all)),
'Mínimo': float(np.nanmin(vals_all)),
'Media': float(np.nanmean(vals_all)),
'Varianza': float(np.nanvar(vals_all))
}
else:
results['Global'] = {'Número': 0, 'Máximo': np.nan, 'Mínimo': np.nan, 'Media': np.nan, 'Varianza': np.nan}
# Montar DataFrame y mostrar
df_stats_model = pd.DataFrame(results).T[['Número', 'Máximo', 'Mínimo', 'Media', 'Varianza']]
display(df_stats_model.round(2))
| Número | Máximo | Mínimo | Media | Varianza | |
|---|---|---|---|---|---|
| UG 2 | 73198.0 | 0.70 | 0.00 | 0.06 | 0.00 |
| UG 3 | 13488.0 | 0.74 | 0.01 | 0.13 | 0.01 |
| UG 4 | 124567.0 | 2.48 | 0.01 | 0.23 | 0.01 |
| Global | 211253.0 | 2.48 | 0.00 | 0.16 | 0.02 |
[ ]:
# Ensure renderer is appropriate for your environment (notebook / vscode)
pio.renderers.default = 'notebook'
# --- Input assumptions (these must exist in the environment) ---
# df: pandas DataFrame with columns 'X','Y','Z'
# categoria_UG4: numpy array with shape (nz, ny, nx) containing strings: 'Inferido','Indicado','Medido'
# x_coords, y_coords, z_coords: 1D coordinate arrays for grid cell centers (lengths nx, ny, nz)
# icx, icy, icz: integer indices for the central slices to display
# nz, ny, nx: grid dimensions
# Map categories to numeric codes
cat_to_num = {'Inferido': 0, 'Indicado': 1, 'Medido': 2}
cat_num = np.vectorize(lambda x: cat_to_num.get(x, np.nan))(modelo_categorias_final)
cat_num = cat_num.astype(float) # allow NaNs
# Create slices (same convention used in the notebook)
x_slice = np.flip(np.flipud(cat_num[:, :, icx]), axis=1) # X-slice (nz, ny)
y_slice = np.flip(cat_num[:, icy, :], axis=0) # Y-slice (nz, nx)
z_slice = np.flip(cat_num[nz-icz, :, :], axis=0) # Z-slice (ny, nx)
# Discrete colors: Inferido (red), Indicado (yellow), Medido (green)
# We use normalized stops [0.0, 0.5, 1.0] so numeric codes 0,1,2 map exactly to the three colors
colorscale = [
[0.0, '#d73027'], # Inferido -> 0
[0.5, "#fff700"], # Indicado -> 1 (normalized to 0.5)
[1.0, '#1a9850'] # Medido -> 2 (normalized to 1.0)
]
# Build surface for X-slice
YY, ZZ = np.meshgrid(y_coords, z_coords) # shapes (nz, ny)
XX = np.full_like(YY, x_coords[icx])
surf_x = go.Surface(
x=XX, y=YY, z=ZZ,
surfacecolor=x_slice.astype(float),
colorscale=colorscale,
cmin=0, cmax=2,
showscale=False,
opacity=1,
name=f'corte X={x_coords[icx]:.1f}',
hoverinfo='skip',
hovertemplate=None
)
# Build surface for Y-slice
XX, ZZ = np.meshgrid(x_coords, z_coords) # shapes (nz, nx)
YY = np.full_like(XX, y_coords[icy])
surf_y = go.Surface(
x=XX, y=YY, z=ZZ,
surfacecolor=y_slice.astype(float),
colorscale=colorscale,
cmin=0, cmax=2,
showscale=False,
opacity=1,
name=f'corte Y={y_coords[icy]:.1f}',
hoverinfo='skip',
hovertemplate=None
)
# Build surface for Z-slice (we enable a colorbar here with categorical ticks)
XX, YY = np.meshgrid(x_coords, y_coords) # shapes (ny, nx)
ZZ = np.full_like(XX, z_coords[icz])
surf_z = go.Surface(
x=XX, y=YY, z=ZZ,
surfacecolor=z_slice.astype(float),
colorscale=colorscale,
cmin=0, cmax=2,
showscale=True,
colorbar=dict(
title='Categoría',
tickvals=[0, 1, 2],
ticktext=['Inferido', 'Indicado', 'Medido'],
thickness=20
),
opacity=1,
name=f'corte Z={z_coords[icz]:.1f}',
hoverinfo='skip',
hovertemplate=None
)
# Add sample points as neutral black markers for reference
scatter_samples = go.Scatter3d(
x=df['X'], y=df['Y'], z=df['Z'],
mode='markers',
marker=dict(color='black', size=2, opacity=0.8),
name='Muestras'
)
# Compose figure
fig = go.Figure(data=[scatter_samples, surf_x, surf_y, surf_z])
fig.update_layout(
title='Categoría (Inferido=rojo, Indicado=amarillo, Medido=verde)',
scene=dict(
xaxis=dict(title='Este'),
yaxis=dict(title='Norte'),
zaxis=dict(title='Elevación')
),
width=900,
height=720,
font=dict(family='Times New Roman', size=12)
)
fig.show()
[89]:
# Entregar para cada UG y cada categoría las estadísticas (Número, Max, Min, Mean, Variance) de modelo_leyes_final
from IPython.display import display
ugs = [2, 3, 4]
categories = ['Inferido', 'Indicado', 'Medido']
results = {}
for ug in ugs:
for cat in categories:
mask = (modelo_ug == ug) & (modelo_categorias_final == cat) & (modelo_leyes_final > 0)
vals = modelo_leyes_final[mask]
cnt = vals.size
key = f'UG {ug} - {cat}'
if cnt > 0:
vals = vals.astype(float)
results[key] = {
'Número': int(cnt),
'Máximo': float(np.nanmax(vals)),
'Mínimo': float(np.nanmin(vals)),
'Media': float(np.nanmean(vals)),
'Varianza': float(np.nanvar(vals))
}
else:
results[key] = {'Número': 0, 'Máximo': np.nan, 'Mínimo': np.nan, 'Media': np.nan, 'Varianza': np.nan}
# Montar DataFrame y mostrar
df_stats_cat = pd.DataFrame(results).T[['Número', 'Máximo', 'Mínimo', 'Media', 'Varianza']]
display(df_stats_cat.round(2))
| Número | Máximo | Mínimo | Media | Varianza | |
|---|---|---|---|---|---|
| UG 2 - Inferido | 1430.0 | 0.43 | 0.00 | 0.04 | 0.00 |
| UG 2 - Indicado | 8147.0 | 0.53 | 0.00 | 0.05 | 0.00 |
| UG 2 - Medido | 63621.0 | 0.70 | 0.00 | 0.06 | 0.00 |
| UG 3 - Inferido | 295.0 | 0.43 | 0.03 | 0.08 | 0.00 |
| UG 3 - Indicado | 1024.0 | 0.44 | 0.01 | 0.10 | 0.00 |
| UG 3 - Medido | 12169.0 | 0.74 | 0.01 | 0.13 | 0.01 |
| UG 4 - Inferido | 2195.0 | 0.55 | 0.03 | 0.20 | 0.01 |
| UG 4 - Indicado | 19573.0 | 0.68 | 0.03 | 0.22 | 0.01 |
| UG 4 - Medido | 102799.0 | 2.48 | 0.01 | 0.24 | 0.01 |
[ ]:
# Entregar para cada UG y cada categorías global = med+ind+inf y demostrado = med + ind las estadísticas (Número, Max, Min, Mean, Variance) de modelo_leyes_final
from IPython.display import display
ugs = [2, 3, 4]
categories = [['Inferido', 'Indicado', 'Medido'], ['Indicado', 'Medido']]
category_labels = ['Global', 'Demostrado']
results = {}
for ug in ugs:
for cat, label in zip(categories, category_labels):
mask = (modelo_ug == ug) & np.isin(modelo_categorias_final, cat) & (modelo_leyes_final > 0) & np.isfinite(modelo_leyes_final)
vals = modelo_leyes_final[mask]
cnt = vals.size
key = f'UG {ug} - {label}'
if cnt > 0:
vals = vals.astype(float)
results[key] = {
'Número': int(cnt),
'Máximo': float(np.nanmax(vals)),
'Mínimo': float(np.nanmin(vals)),
'Media': float(np.nanmean(vals)),
'Varianza': float(np.nanvar(vals))
}
else:
results[key] = {'Número': 0, 'Máximo': np.nan, 'Mínimo': np.nan, 'Media': np.nan, 'Varianza': np.nan}
# Montar DataFrame y mostrar
df_stats_cat = pd.DataFrame(results).T[['Número', 'Máximo', 'Mínimo', 'Media', 'Varianza']]
display(df_stats_cat.round(2))
| Número | Máximo | Mínimo | Media | Varianza | |
|---|---|---|---|---|---|
| UG 2 - Global | 73198.0 | 0.70 | 0.00 | 0.06 | 0.00 |
| UG 2 - Demostrado | 71768.0 | 0.70 | 0.00 | 0.06 | 0.00 |
| UG 3 - Global | 13488.0 | 0.74 | 0.01 | 0.13 | 0.01 |
| UG 3 - Demostrado | 13193.0 | 0.74 | 0.01 | 0.13 | 0.01 |
| UG 4 - Global | 124567.0 | 2.48 | 0.01 | 0.23 | 0.01 |
| UG 4 - Demostrado | 122372.0 | 2.48 | 0.01 | 0.23 | 0.01 |
[94]:
## Curva Tonelaje (izq.) y ley promedio (der.) in secondary axis para el modelo final de leyes overlayed in the same plot
import plotly.graph_objects as go
# Prepare data
cutoff_values = np.arange(0, df['cu_pct'].quantile(0.96), 0.01)
mtonnage = []
average_grade = []
for cutoff in cutoff_values:
mask = (modelo_leyes_final >= cutoff) & np.isfinite(modelo_leyes_final)
## puede crear otra mascara mask para filtrar por categoría o UG si lo desea
mask = np.logical_and(mask, np.logical_or(modelo_categorias_final == 'Medido', modelo_categorias_final == 'Indicado', modelo_categorias_final == 'Inferido')) # ejemplo: solo categoría Medido
## mask = np.logical_and(mask, modelo_ug == 4) # ejemplo: solo UG 4. COMBINAR AMBOS FILTROS SI ES NECESARIO
total_tonnage = np.sum(mask) * (xsiz * ysiz * zsiz * 2.5) # assuming 2.5 t/m³ (2.5 g/cm³)
total_mtonnage = total_tonnage / 1e6 # convert to million tonnes
mtonnage.append(round(total_mtonnage, 2))
if total_tonnage > 0:
avg_grade = np.nanmean(modelo_leyes_final[mask])
else:
avg_grade = 0
average_grade.append(round(avg_grade, 2))
# Create figure with secondary y-axis
fig = go.Figure()
# Tonnage trace
fig.add_trace(
go.Scatter(
x=cutoff_values,
y=mtonnage,
name='Tonelaje',
line=dict(color='blue'),
yaxis='y1'
)
)
# Average grade trace
fig.add_trace(
go.Scatter(
x=cutoff_values,
y=average_grade,
name='Ley Promedio (%)',
line=dict(color='red'),
yaxis='y2'
)
)
# Update layout for dual y-axes
fig.update_layout(
title='Curva Tonelaje y Ley Promedio vs Corte de Ley (Modelo Final de Leyes)',
xaxis=dict(title='Corte de Ley (%)',gridcolor='rgba(0,0,0,0.2)'),
yaxis=dict(
title='Tonelaje (MTon)',
tickfont=dict(color='blue',
),
side='left',
gridcolor='rgba(0,0,0,0.2)'
),
yaxis2=dict(
title='Ley Promedio (%)',
tickfont=dict(color='red'),
tickformat=',.f',
overlaying='y',
side='right'
),
legend=dict(x=0.1, y=0.9),
font_family="Times New Roman",
font_size=12,
font_color="black",
plot_bgcolor='white',
width=800,
height=500
)
fig.show()
[95]:
!conda install -c conda-forge ezdxf scipy -y
3 channel Terms of Service accepted
Retrieving notices: done
Channels:
- conda-forge
- defaults
Platform: win-64
Collecting package metadata (repodata.json): done
Solving environment: done
## Package Plan ##
environment location: C:\Users\Alvaro\anaconda3\envs\min431
added / updated specs:
- ezdxf
- scipy
The following packages will be downloaded:
package | build
---------------------------|-----------------
ezdxf-1.4.2 | py313h7084e01_1 2.7 MB conda-forge
scipy-1.16.3 | py313h62a08ca_0 14.6 MB conda-forge
------------------------------------------------------------
Total: 17.3 MB
The following NEW packages will be INSTALLED:
ezdxf conda-forge/win-64::ezdxf-1.4.2-py313h7084e01_1
The following packages will be UPDATED:
scipy 1.16.0-py313h97dfcff_0 --> 1.16.3-py313h62a08ca_0
Downloading and Extracting Packages: ...working...
scipy-1.16.3 | 14.6 MB | | 0%
ezdxf-1.4.2 | 2.7 MB | | 0%
scipy-1.16.3 | 14.6 MB | | 0%
scipy-1.16.3 | 14.6 MB | 5 | 6%
scipy-1.16.3 | 14.6 MB | #2 | 13%
ezdxf-1.4.2 | 2.7 MB | | 1%
scipy-1.16.3 | 14.6 MB | ##3 | 24%
ezdxf-1.4.2 | 2.7 MB | ###5 | 35%
scipy-1.16.3 | 14.6 MB | ###3 | 33%
ezdxf-1.4.2 | 2.7 MB | ########## | 100%
ezdxf-1.4.2 | 2.7 MB | ########## | 100%
scipy-1.16.3 | 14.6 MB | ####5 | 45%
scipy-1.16.3 | 14.6 MB | #####7 | 57%
scipy-1.16.3 | 14.6 MB | ######8 | 68%
scipy-1.16.3 | 14.6 MB | ########2 | 82%
scipy-1.16.3 | 14.6 MB | #########8 | 98%
scipy-1.16.3 | 14.6 MB | ########## | 100%
ezdxf-1.4.2 | 2.7 MB | ########## | 100%
scipy-1.16.3 | 14.6 MB | ########## | 100%
done
Preparing transaction: done
Verifying transaction: done
Executing transaction: done
==> WARNING: A newer version of conda exists. <==
current version: 25.7.0
latest version: 25.9.1
Please update conda by running
$ conda update -n base -c conda-forge conda
[ ]:
"""
Flag blocks below topography from a DXF surface.
Funciona con DXF que contenga 3DFACE (malla) o polylines/contours (extrae vértices).
Salida: nuevo CSV con columnas adicionales Z_topo y Below_Topo (True/False).
Requisitos: ezdxf, numpy, pandas, scipy
"""
import ezdxf
import numpy as np
import pandas as pd
from scipy.interpolate import LinearNDInterpolator
from scipy.spatial import cKDTree
import sys
import os
ZZ, YY, XX = np.meshgrid(z_coords, y_coords, x_coords) # shapes (ny, nx, nz)
## to csv
grid_points = np.column_stack([XX.ravel(), YY.ravel(), ZZ.ravel()]) # shape (N, 3) where N = nx*ny*nz
np.savetxt("grid_points.csv", grid_points, delimiter=",", header="X,Y,Z", comments="")
# ====== CONFIG ======
DXF_FILE = "topo.dxf" # <- cambia al path de tu DXF
BLOCKS_CSV = "grid_points.csv" # <- cambia al path de tu CSV
OUTPUT_CSV = "block_model_flagged.csv"
# Nombre de columnas del block model
X_COL = "X"
Y_COL = "Y"
Z_COL = "Z"
TOL = 1e-9 # tolerancia para comparaciones; ajústala si quieres
# ====================
def extract_surface_points_from_dxf(dxf_path):
"""
Extrae puntos (x,y,z) desde 3DFACE, POLYLINE, LWPOLYLINE, POINT, VERTEX.
Devuelve numpy array (N,3).
"""
doc = ezdxf.readfile(dxf_path)
msp = doc.modelspace()
pts = []
# 3DFACE y 3dface (varía según ezdxf versiones)
try:
faces = msp.query("3DFACE")
except Exception:
faces = msp.query("3dface")
for f in faces:
# cada face puede tener up to 4 vértices
try:
for v in f.vertices:
pts.append((float(v[0]), float(v[1]), float(v[2])))
except Exception:
# fallback: usar atributos dxf
for i in range(1,5):
key = f"dxf.vtx{i}"
if hasattr(f.dxf, f"vtx{i}"):
v = getattr(f.dxf, f"vtx{i}")
pts.append((float(v[0]), float(v[1]), float(v[2])))
# LWPOLYLINE y POLYLINE (líneas de contorno)
for pl in msp.query("LWPOLYLINE"):
for p in pl.get_points():
# get_points devuelve (x,y,bulge) o (x,y) o (x,y,z) - toleramos
if len(p) >= 2:
x, y = float(p[0]), float(p[1])
z = None
# algunos LWPOLYLINE no tienen z; si existe, ezdxf puede guardarlo en .attribs o en p[2]
if len(p) >= 3:
try:
z = float(p[2])
except Exception:
z = None
# si z es None, trataremos luego; por ahora, omitimos o podemos asignar 0 (mejor omitir)
if z is not None:
pts.append((x, y, z))
# POLYLINE clásicas (3D POLYLINE)
for pl in msp.query("POLYLINE"):
for v in pl.vertices():
coords = v.dxf.location if hasattr(v.dxf, "location") else v
try:
x, y, z = float(coords[0]), float(coords[1]), float(coords[2])
pts.append((x, y, z))
except Exception:
# si no hay Z explícito, ignoramos
pass
# POINT entities
for p in msp.query("POINT"):
try:
x, y, z = p.dxf.location
pts.append((float(x), float(y), float(z)))
except Exception:
pass
pts = np.array(pts, dtype=float)
if pts.size == 0:
raise ValueError("No se encontraron puntos 3D en el DXF. Asegúrate de que el DXF tenga 3DFACE o polylines con Z.")
# deduplicate (opcional)
pts = np.unique(pts, axis=0)
return pts
def build_interpolator(surface_pts):
"""
Construye un interpolador LinearNDInterpolator sobre los puntos de la superficie.
Devuelve función interp(x_array, y_array) -> z_array
"""
xy = surface_pts[:, :2]
z = surface_pts[:, 2]
interp = LinearNDInterpolator(xy, z, fill_value=np.nan)
# KDTree para nearest neighbor fill
tree = cKDTree(xy)
def interp_with_nn(xq, yq):
xq = np.asarray(xq)
yq = np.asarray(yq)
qpts = np.column_stack([xq.ravel(), yq.ravel()])
zq = interp(qpts)
# interp puede devolver array con shape (N,); si NaN en puntos fuera del convex hull, rellenar con nearest
nan_mask = np.isnan(zq)
if np.any(nan_mask):
dists, idx = tree.query(qpts[nan_mask], k=1)
zq[nan_mask] = z[idx]
return zq.reshape(xq.shape)
return interp_with_nn
def main(dxf_file, blocks_csv, output_csv):
print("Leyendo DXF y extrayendo puntos de superficie...")
surface_pts = extract_surface_points_from_dxf(dxf_file)
print(f"Puntos extraídos de la superficie: {surface_pts.shape[0]}")
print("Construyendo interpolador de la superficie...")
interp = build_interpolator(surface_pts)
print("Leyendo block model CSV...")
df = pd.read_csv(blocks_csv)
if X_COL not in df.columns or Y_COL not in df.columns or Z_COL not in df.columns:
print("Columnas encontradas en CSV:", df.columns.tolist())
raise KeyError(f"El CSV debe contener columnas '{X_COL}', '{Y_COL}', '{Z_COL}'. Ajusta X_COL/Y_COL/Z_COL en el script si tus nombres son diferentes.")
# calcular topo en cada bloque
print("Interpolando elevación topo para cada bloque...")
xq = df[X_COL].values
yq = df[Y_COL].values
z_topo = interp(xq, yq)
df["Z_topo"] = z_topo
# Si hay NaNs (no debería haber por nearest fill), tratarlos:
if df["Z_topo"].isna().any():
print("Advertencia: hay valores Z_topo NaN. Rellenando con nearest neighbor simple.")
# fallback: nearest neighbor from surface points
tree = cKDTree(surface_pts[:, :2])
idx = tree.query(np.column_stack([xq, yq]), k=1)[1]
df.loc[df["Z_topo"].isna(), "Z_topo"] = surface_pts[idx[df["Z_topo"].isna()], 2]
# flag: block Z < topo Z (puedes invertir el < si tus Z están ordenadas al revés)
df["Below_Topo"] = df[Z_COL].astype(float) < (df["Z_topo"].astype(float) - TOL)
print(f"Bloques totales: {len(df)}. Bloques below topo: {df['Below_Topo'].sum()}")
print(f"Guardando resultado en: {output_csv}")
## convet df["Below_Topo"] to 1/0
df["Below_Topo"] = df["Below_Topo"].astype(int)
## copy only df["Below_Topo"] in a new variable appending 1 in the first line
below_topo = pd.Series([1] + df["Below_Topo"].tolist())
below_topo.to_csv(output_csv, index=False)
print("Listo.")
if __name__ == "__main__":
# si quieres pasar argumentos por línea de comando:
# python flag_blocks_below_topo.py topography.dxf block_model.csv output.csv
if len(sys.argv) >= 4:
DXF_FILE = sys.argv[1]
BLOCKS_CSV = sys.argv[2]
OUTPUT_CSV = sys.argv[3]
main(DXF_FILE, BLOCKS_CSV, OUTPUT_CSV)
Leyendo DXF y extrayendo puntos de superficie...
Puntos extraídos de la superficie: 14177
Construyendo interpolador de la superficie...
Leyendo block model CSV...
Interpolando elevación topo para cada bloque...
Bloques totales: 1241600. Bloques below topo: 1083384
Guardando resultado en: block_model_flagged.csv
Listo.
[131]:
## leer modelo flageado
modelo_flagged = GSLIB.GSLIB2ndarray_3D("block_model_flagged.csv", 0, 1, nx, ny, nz)[0][0]
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
Cell In[131], line 2
1 ## leer modelo flageado
----> 2 modelo_flagged = GSLIB.GSLIB2ndarray_3D("block_model_flagged.csv", 0, 1, nx, ny, nz)[0][0]
File c:\Users\Alvaro\anaconda3\envs\min431\Lib\site-packages\geostatspy\GSLIB.py:143, in GSLIB2ndarray_3D(data_file, kcol, nreal, nx, ny, nz)
141 for iy in range(ny):
142 for ix in range(nx):
--> 143 head = next(f)
144 array[ineal][nz - 1 - iz][ny - 1 - iy][ix] = head.split()[kcol]
145 elif ny > 1:
StopIteration:
[126]:
continuous = 'cu_pct'
x_coords = np.linspace(xmin + xsiz/2, xmax - xsiz/2, nx)
y_coords = np.linspace(ymin + ysiz/2, ymax - ysiz/2, ny)
z_coords = np.linspace(zmin + zsiz/2, zmax - zsiz/2, nz)
import plotly.io as pio
# pio.renderers.default = "colab"
pio.renderers.default = "notebook" # usar esta linea en jupyter notebook o vscode
import plotly.graph_objects as go
import plotly.express as px
import plotly.offline as py
fig = go.Figure(data=[go.Scatter3d(
x=df['X'],
y=df['Y'],
z=df['Z'],
marker=dict(color=df[continuous],
colorscale=px.colors.sequential.Rainbow[1:],
cmin=0.0,
cmax=df[continuous].quantile(0.95),
size=2.0, # Esta opcion controla el tamaño de los datos
colorbar=dict(
title='Cu [%]',
thickness=20,
# Add a border to the colorbar
outlinecolor='black', # Color of the border
outlinewidth=2, # Width of the border
bordercolor='white', # Background border color (if applicable)
borderwidth=1 # Background border width
)
),
mode='markers',
opacity=1
)])
# otras opciones para controlar el color de los elementos de la figura, borrar hovermode para desactivar etiquetas al pasar el mouse
fig.update_layout( title_text='Estimación de Cu - UG 4',
scene = dict(
xaxis = dict(
title='Este',
backgroundcolor="white",
gridcolor="gray",
showbackground=True,
zerolinecolor="white",),
yaxis = dict(
title='Norte',
backgroundcolor="white",
gridcolor="gray",
showbackground=True,
zerolinecolor="white"),
zaxis = dict(
title='Elevación',
backgroundcolor="white",
gridcolor="gray",
showbackground=True,
zerolinecolor="white",),
),
font_family="Times New Roman",
font_color="black", hovermode=False
)
# ocultar la leyenda/autonombre "trace 0" junto al colorbar
fig.update_layout(showlegend=False)
# definir cortes en x, y, z
icz = 60 # índice de corte en z (puede ajustarse), va entre 0 y nz-1
# Agregar cortes del modelo de UGs con la misma escala de colores de plotly
x_slice = np.flip(np.flipud(modelo_leyes_final_flagged[:, :, icx]), axis=1)
y_slice = np.flip(modelo_leyes_final_flagged[:, icy, :], axis=0)
z_slice = np.flip(modelo_leyes_final_flagged[nz-icz, :, :], axis=0)
# X-slice (plano constante x)
YY, ZZ = np.meshgrid(y_coords, z_coords) # shapes (nz, ny)
XX = np.full_like(YY, x_coords[icx])
## nan if above topo (bm_flagged) as nan
XX[x_slice == np.nan] = np.nan
surf_x = go.Surface(
x=XX, y=YY, z=ZZ,
surfacecolor=x_slice.astype(float),
colorscale=px.colors.sequential.Rainbow[1:],
cmin=0.0,
cmax=df[continuous].quantile(0.95),
showscale=False,
opacity=1,
name=f'corte X={x_coords[icx]:.1f}',
hoverinfo='skip',
hovertemplate=None
)
# Y-slice (plano constante y)
XX, ZZ = np.meshgrid(x_coords, z_coords) # shapes (nz, nx)
YY = np.full_like(XX, y_coords[icy])
## nan if above topo (bm_flagged) as nan
YY[y_slice == np.nan] = np.nan
surf_y = go.Surface(
x=XX, y=YY, z=ZZ,
surfacecolor=y_slice.astype(float),
colorscale=px.colors.sequential.Rainbow[1:],
cmin=0.0,
cmax=df[continuous].quantile(0.95),
showscale=False,
opacity=1,
name=f'corte Y={y_coords[icy]:.1f}',
hoverinfo='skip',
hovertemplate=None,
)
# Z-slice (plano constante z)
XX, YY = np.meshgrid(x_coords, y_coords) # shapes (ny, nx)
ZZ = np.full_like(XX, z_coords[icz])
surf_z = go.Surface(
x=XX, y=YY, z=ZZ,
surfacecolor=z_slice.astype(float),
colorscale=px.colors.sequential.Rainbow[1:],
cmin=0.0,
cmax=df[continuous].quantile(0.95),
showscale=False,
opacity=1,
name=f'corte Z={z_coords[icz]:.1f}',
hoverinfo='skip',
hovertemplate=None
)
# Añadir superficies al figure (manteniendo la leyenda y colores consistentes)
fig.add_trace(surf_x)
fig.add_trace(surf_y)
fig.add_trace(surf_z)
## add 3d mesh with topography surface from dxf file for reference
topo_pts = extract_surface_points_from_dxf("topo.dxf")
topo_mesh = go.Mesh3d(
x=topo_pts[:, 0],
y=topo_pts[:, 1],
z=topo_pts[:, 2],
color='lightgrey',
opacity=0.5,
name='Topografía (DXF)',
hoverinfo='skip'
)
fig.add_trace(topo_mesh)
fig.show()